GPU Performance Optimization

算子优化介绍

Kernel、存储层次与算子融合

核心结论
一、核心概念

K Kernel(内核)

在 GPU 上执行的函数,是 GPU 编程的基本执行单元。算子是逻辑概念,Kernel 是算子的具体实现。

F 算子融合(Operator Fusion)

将多个 Kernel 合并成一个,减少显存读写次数,让中间结果留在寄存器/共享内存中。

二、GPU存储层次(速度从快到慢)
寄存器 Register
~256KB/SM · 线程私有
⚡ ~1 cycle
共享内存 Shared Memory
128-256KB/SM · Block内共享
~20 cycles
L1 / L2 缓存
L1与共享内存共用 · L2 40-60MB全局
~30-200 cycles
显存 VRAM / HBM
24-80GB · 全GPU可访问
~400 cycles
↓ PCIe / NVLink
主机内存 Host Memory
几十~几百GB · CPU端
极高延迟
⬆️
速度更快
容量更小
⬇️
速度更慢
容量更大
三、算子融合的本质
❌ 未融合:3个Kernel
K1 GEMM → 写显存
读显存
K2 Add Bias → 写显存
读显存
K3 GeLU → 写显存
显存访问 256 MB
Kernel调用 3 次
✅ 融合后:1个Kernel
F Fused GEMM + Bias + GeLU
├─ Tensor Core: GEMM
├─ 寄存器传递中间结果
└─ CUDA Core: Bias + GeLU
写显存(仅最终结果)
显存访问 96 MB(减少62%)
Kernel调用 1 次
核心洞察:显存访问是GPU性能瓶颈!算子融合的本质是减少显存读写,让数据尽量留在寄存器共享内存中完成多步计算。融合后的 Kernel 内部,Tensor CoreCUDA Core 协作执行,中间结果通过寄存器传递。
01

算子融合与硬件的关系

算子融合是软件层面的优化策略,Tensor Core / CUDA Core是硬件执行单元。融合后的 Kernel 内部会协调使用两种硬件。

软件层
算子融合决策
决定哪些操作合并成一个 Kernel(由编译器/框架/手工实现)
Kernel层
融合后的 Kernel
__global__ void fused_gemm_bias_gelu(...)
调度层
Kernel 内部硬件调度
GEMM 部分 → Tensor Core · Bias + GeLU → CUDA Core
中间结果在寄存器传递,不写回显存
硬件层
执行单元
Tensor Core(矩阵乘法) + CUDA Core(逐元素操作)

硬件级融合 vs 软件级融合

层次 名称 示例 执行单元
硬件级 FMA (Fused Multiply-Add) D = A×B + C(单条指令) Tensor Core
软件级 算子融合 (Operator Fusion) GEMM + Bias + GeLU → 1个 Kernel Tensor Core + CUDA Core

常用术语全称

缩写 全称 层次 说明
FMA Fused Multiply-Add 指令级 单条硬件指令完成 D = A×B + C
GEMM General Matrix Multiply 算子级 通用矩阵乘法,由大量 FMA 操作组成
GeLU Gaussian Error Linear Unit 算子级 高斯误差线性单元,Transformer 常用激活函数
SiLU Sigmoid Linear Unit 算子级 又称 Swish,SiLU(x) = x × σ(x)
HBM High Bandwidth Memory 硬件 高带宽显存,现代 GPU 使用
SM Streaming Multiprocessor 硬件 流式多处理器,GPU 基本计算单元

层次关系:FMA 是硬件指令(砖头),GEMM 是由大量 FMA 组成的算子(墙),算子融合是将多个算子合并成一个 Kernel(房子)。

02

什么是 Kernel?

Kernel 是在 GPU 上执行的函数,由 CPU 发起调用,在 GPU 的成千上万个线程上并行执行。

// 这就是一个 Kernel —— 使用 __global__ 关键字声明
__global__ void add(float *a, float *b, float *c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) c[i] = a[i] + b[i];
}

// CPU 调用 Kernel,在 GPU 上并行执行
add<<>>(a, b, c, n);

CUDA 函数类型

标识符 执行位置 调用者 说明
__global__ GPU CPU Kernel 函数,GPU 并行执行
__device__ GPU GPU 设备函数,只能被 Kernel 调用
__host__ CPU CPU 普通 CPU 函数(默认)

Kernel vs 算子

概念 层次 示例
算子 (Operator) 逻辑概念 MatMul, ReLU, LayerNorm, Softmax
Kernel 具体实现 __global__ void matmul_kernel(...)

一个算子可能由一个或多个 Kernel 实现。例如,复杂的 MatMul 可能拆分为多个 Kernel 处理不同的分块。

03

GPU 存储层次详解

存储类型 容量 延迟 带宽 作用域 程序员可控
寄存器 ~256KB/SM 1 cycle 最高 线程私有 隐式(局部变量)
共享内存 128-256KB/SM ~20 cycles ~19 TB/s Block 内共享 __shared__
L1 缓存 与共享内存共用 ~30 cycles ~19 TB/s SM 内 部分可控
L2 缓存 40-60MB ~200 cycles ~5 TB/s 全 GPU 自动
显存 (HBM) 24-80GB ~400 cycles ~3 TB/s 全 GPU ✅ 显式分配
主机内存 几百GB 极高 ~64 GB/s CPU 端

Kernel 执行时的数据流动

典型 Kernel 数据流
显存
Global Memory
寄存器
加载数据
ALU / Tensor Core
计算
寄存器
结果暂存
显存
写回结果
需要线程协作时的数据流(如规约、分块矩阵乘法)
显存
共享内存
线程间共享
寄存器
计算
寄存器
如需线程间交换中间结果
寄存器
共享内存
线程协作
寄存器
显存

关键区别:
寄存器:线程私有,用于暂存单线程的计算结果
共享内存:Block 内共享,用于线程间数据交换(如规约、分块加载)
• 单线程独立计算时:结果直接存寄存器,不经过共享内存

__global__ void example(float *input, float *output) {
    // 1. 声明共享内存(Block 内线程共享)
    __shared__ float tile[256];
    
    // 2. 从显存读取到共享内存
    tile[threadIdx.x] = input[blockIdx.x * 256 + threadIdx.x];
    __syncthreads();  // 同步,确保数据加载完成
    
    // 3. 从共享内存读到寄存器,进行计算
    float val = tile[threadIdx.x];  // val 在寄存器中
    val = val * 2.0f + 1.0f;        // 计算在寄存器中完成
    
    // 4. 结果写回显存
    output[blockIdx.x * 256 + threadIdx.x] = val;
}
04

规约操作(Reduction)

规约是一种将多个数据通过某种操作合并成一个值的计算模式,在深度学习中非常常见。

规约示意:多个值 → 一个值
[a, b, c, d, e, f, g, h]
规约操作
如:求和
单个值

常见的规约操作

规约类型 操作 示例
Sum 求和 [1, 2, 3, 4] → 10
Max 求最大值 [1, 5, 3, 2] → 5
Min 求最小值 [4, 2, 6, 1] → 1
Product 求积 [2, 3, 4] → 24
Mean 求均值 [2, 4, 6, 8] → 5
LogSumExp Softmax 中用 log(Σexp(xᵢ))

深度学习中的规约

# LayerNorm 中的规约
mean = x.mean(dim=-1)      # 规约:求均值
var = x.var(dim=-1)        # 规约:求方差

# Softmax 中的规约
max_val = x.max(dim=-1)    # 规约:求最大值(数值稳定性)
sum_exp = exp(x - max_val).sum(dim=-1)  # 规约:求和

# Loss 计算
loss = losses.mean()       # 规约:batch 内求均值

# Attention
attn_weights = softmax(scores, dim=-1)  # 内含 max 和 sum 规约

规约为什么需要线程协作?

因为数据分布在不同线程中,要得到全局结果必须线程间通信,这就需要用到共享内存

GPU 并行规约过程(树形规约)
Step 0: 每个线程持有一个数据
T0: a T1: b T2: c T3: d T4: e T5: f T6: g T7: h
Step 1: 相邻线程配对相加
T0: a+b T1: - T2: c+d T3: - T4: e+f T5: - T6: g+h T7: -
Step 2: 继续配对
T0: a+b+c+d T1-T3: - T4: e+f+g+h T5-T7: -
Step 3: 最终结果
T0: a+b+c+d+e+f+g+h ✓

GPU 并行规约代码

__shared__ float sdata[256];  // 共享内存,用于线程间通信

// 1. 每个线程加载自己的数据到共享内存
sdata[tid] = input[i];
__syncthreads();  // 同步,确保所有线程加载完成

// 2. 并行规约 - 树形结构,每轮活跃线程减半
for (int s = blockDim.x / 2; s > 0; s >>= 1) {
    if (tid < s) {
        sdata[tid] += sdata[tid + s];  // 读取其他线程的数据并累加
    }
    __syncthreads();  // 每轮都要同步
}

// 3. 线程0持有最终结果,写回显存
if (tid == 0) output[blockIdx.x] = sdata[0];

概念区分:规约 vs 协作

概念 含义 关系
规约 (Reduction) 多个值 → 一个值的计算模式 算法/计算模式("做什么")
协作 (Cooperation) 多个线程共同完成任务 实现方式("怎么做")
共享内存 Block 内线程可共同访问的存储 协作的工具
__syncthreads() 等待所有线程到达同一点 协作的保障

总结:规约是"做什么"(计算模式),协作是"怎么做"(实现方式)。规约通常需要协作来实现,但协作不仅仅用于规约——比如分块矩阵乘法也需要协作加载数据到共享内存。

05

为什么显存访问是瓶颈?

H100 计算能力 vs 显存带宽
Tensor Core 算力
1,979 TFLOPS
显存带宽
3.35 TB/s

计算/访存比分析

计算 1 次 FP16 乘加需要读取 4 字节
理论比值:1979T ÷ (3.35T/4) ≈ 2400:1

如果每次计算都访问显存,99.9% 的时间在等数据!

优化核心原则

✅ 减少显存访问次数
✅ 数据尽量留在寄存器/共享内存
✅ 多个操作合并到一个 Kernel
✅ 提高计算密度(每次访存做更多计算)

06

算子融合代码示例

// ❌ 未融合:两个独立 Kernel,两次显存读写
__global__ void add(float *a, float *b, float *c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) c[i] = a[i] + b[i];
}

__global__ void relu(float *x, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) x[i] = x[i] > 0 ? x[i] : 0;
}

// 调用两次 Kernel
add<<>>(a, b, c, n);  // 写 c 到显存
relu<<>>(c, n);       // 读 c,再写回
// ✅ 融合后:一个 Kernel,一次显存写入
__global__ void add_relu(float *a, float *b, float *c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        float sum = a[i] + b[i];       // sum 在寄存器中
        c[i] = sum > 0 ? sum : 0;     // 直接计算后写出
    }
}

// 只调用一次 Kernel
add_relu<<>>(a, b, c, n);

深度学习中的典型融合模式

# PyTorch 原始写法(3个独立算子)
x = conv(input)      # 卷积: 读 input, 写 x
x = batch_norm(x)    # 归一化: 读 x, 写 x
x = relu(x)          # 激活: 读 x, 写 x

# 融合后(1个融合算子)
x = conv_bn_relu(input)  # 读 input, 写 x
                         # 中间结果在寄存器中传递

常见融合模式

融合模式 算子组合 应用场景
Conv-BN-ReLU 卷积 + 归一化 + 激活 CNN
GEMM-Bias-Act 矩阵乘 + 偏置 + 激活 Transformer MLP
LayerNorm-QKV 归一化 + 线性投影 Attention
Softmax-Mask-Scale 缩放 + 掩码 + Softmax Attention
Add-LayerNorm 残差连接 + 归一化 Transformer
07

算子优化工具与加速库

TensorRT

NVIDIA 推理优化器,自动算子融合

XLA

TensorFlow/JAX 编译器

TorchScript

PyTorch JIT 编译

Triton

OpenAI 开源,Python 写 Kernel

CUTLASS

NVIDIA CUDA 模板库

FlashAttention

Attention 算子深度优化

加速库 = 优化算子的集合

加速库 提供的算子 领域
cuBLAS GEMM, GEMV, DOT, AXPY... 线性代数
cuDNN Conv, Pool, BN, ReLU, Attention... 深度学习
cuFFT FFT, IFFT... 傅里叶变换
cuSPARSE SpMV, SpMM... 稀疏矩阵
应用层
PyTorch / TensorFlow
torch.mm(A, B)
加速库
cuBLAS / cuDNN
cublasSgemm(...)
算子实现
高度优化的 CUDA Kernel
__global__ void sgemm_optimized(...)
硬件
CUDA Core / Tensor Core
实际执行计算的硬件单元
总结:算子优化的核心是减少显存访问。通过算子融合,将多个操作合并到一个 Kernel 中,让数据在寄存器共享内存中传递,避免频繁读写显存。加速库(cuBLAS、cuDNN)提供了高度优化的算子实现,框架(PyTorch、TensorFlow)在其基础上构建更高层的 API。